package edu.geo.geomdomain;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.vividsolutions.jts.geom.Geometry;

import edu.geo.containers.geom.impl.GeometryFactory;
import edu.geo.containers.geom.impl.LineSegment;
import edu.geo.containers.geom.impl.Point;
import edu.geo.containers.geomdomain.EGeomDomainClosureConditionType;
import edu.geo.containers.geomdomain.GeometricDomain;
import edu.geo.containers.geomdomain.GeometricDomainFault;
import edu.geo.containers.geomdomain.Grid;
import edu.geo.exception.GeoInfoException;

/**
 * Valida un dominio geométrico, idenficando las condiciones de cierre que no se
 * cumplen para el dominio geométrico especificado. Se debe tener en cuenta que
 * esta clase proporciona funcionalidad que sólo identifica las condiciones que
 * no se cumplen, mas no corrije o ajusta el dominio geométrico especificado
 * como argumento.
 * 
 * @author alejandro.sanchez
 * 
 */
public class GeometricDomainValidator implements Serializable {
	
	/**
	 * 
	 */
	private static final long serialVersionUID = 1358539965138115347L;
	static private final Log LOGGER = LogFactory
			.getLog(GeometricDomainValidator.class);

	/**
	 * Valida el cumplimiento de las condiciones de clausura de un dominio
	 * geométrico. En caso que las condiciones no se cumplan, entonces se
	 * retorna una lista de puntos con las condiciones que se están
	 * incumpliendo.
	 * 
	 * @param geometricDomain
	 *            Un dominio geométrico.
	 * @return Una lista de <localización (punto), condición> que se está
	 *         incumpliendo.
	 * @throws GeoInfoException
	 *             Se lanzará una excepción en caso que se presente un error
	 *             durante la validación del dominio geométrico.
	 */
	public static List<GeometricDomainFault> validate(
			GeometricDomain geometricDomain) throws GeoInfoException {
		List<GeometricDomainFault> faults = new ArrayList<GeometricDomainFault>();
		// First condition
		validateGeometricDomainPoints(geometricDomain, faults);
		//Second condition
		validateLineSegmentEnds(geometricDomain, faults);
		//Third edition
		validateIncidentPoins(geometricDomain, faults);
		//Fourth edition
		validateLineSegmentIntersections(geometricDomain, faults);
		return faults;
	}

	/**
	 * Valida que cada punto del conjunto de puntos del dominio geométrico, haga
	 * parte del dominio de la grilla.
	 * 
	 * @param geometricDomain
	 *            Un dominio geométrico.
	 * @param faults
	 *            Lista donde se agregarán los puntos donde se incumpla la
	 *            primera condición: "Cada punto del conjunto P (correspondiente
	 *            al conjunto de puntos del dominio geométrico), debe hacer
	 *            parte del dominio de la retícula G".
	 * @throws GeoInfoException
	 *             Se lanzará una excepción en caso que se presente un error
	 *             durante la validación.
	 */
	protected static void validateGeometricDomainPoints(
			final GeometricDomain geometricDomain,
			List<GeometricDomainFault> faults) throws GeoInfoException {
		
		for (Point point: geometricDomain.getPoints())
		{
			if ( !verifyPointOnGrid( point, geometricDomain.getGrid()) )
			{
				GeometricDomainFault geometricDomainFault = new GeometricDomainFault( point, EGeomDomainClosureConditionType.POINT_ON_GRID );
				faults.add( geometricDomainFault );
				LOGGER.info("FAULT: " + geometricDomainFault.getLocation().toString() + "-" + geometricDomainFault.getType().getDescription());
			}
		}

	}

	/**
	 * Valida que cada extremo de los segmentos de línea S sean miembros del
	 * conjunto de puntos P del dominio geométrico.
	 * 
	 * @param geometricDomain
	 *            Un dominio geométrico.
	 * @param faults
	 *            Lista donde se agregarán los puntos donde se incumpla la
	 *            segunda condición: "Los extremos de todos los segmentos de
	 *            línea del dominio geométrico deben hacer parte del conjunto P
	 *            (correspondiente al conjunto de puntos del dominio
	 *            geométrico)".
	 * @throws GeoInfoException
	 *             Se lanzará una excepción en caso que se presente un error
	 *             durante la validación.
	 */
	protected static void validateLineSegmentEnds(
			final GeometricDomain geometricDomain,
			List<GeometricDomainFault> faults) throws GeoInfoException {
		
		List<Point> points = geometricDomain.getPoints();
		List<LineSegment> lineSegments = geometricDomain.getLineSegments();
		if (points != null && !points.isEmpty() 
			&& lineSegments != null && !lineSegments.isEmpty())
		{
			for ( LineSegment lineSegment : lineSegments)
			{			
				if (!containsByGeometry(lineSegment.getBeginNode(), points))
				{
					GeometricDomainFault geometricDomainFault = new GeometricDomainFault( lineSegment.getBeginNode(), EGeomDomainClosureConditionType.LINE_SEGMENT_ENDS_ON_P );
					faults.add( geometricDomainFault );
					LOGGER.info("FAULT: " + geometricDomainFault.getLocation().toString() + "-" + geometricDomainFault.getType().getDescription());
					
				}			
				if (!containsByGeometry(lineSegment.getEndNode(), points))
				{
					GeometricDomainFault geometricDomainFault = new GeometricDomainFault( lineSegment.getEndNode(), EGeomDomainClosureConditionType.LINE_SEGMENT_ENDS_ON_P );
					faults.add( geometricDomainFault );
					LOGGER.info("FAULT: " + geometricDomainFault.getLocation().toString() + "-" + geometricDomainFault.getType().getDescription());
				}
			}
		}
	}

	/**
	 * Valida que todos los puntos del conjunto P del dominio geométrico que
	 * sean incidentes con algún(nos) de los segmentos de línea del conjunto S,
	 * correspondan a sus extremos.
	 * 
	 * @param geometricDomain
	 *            Un dominio geométrico.
	 * @param faults
	 *            Lista donde se agregarán los puntos donde se incumpla la
	 *            tercera condición: "Cada punto en P que es incidente con un
	 *            segmento de línea en S deber corresponder a alguno de sus
	 *            extremos".
	 * 
	 * @throws GeoInfoException
	 *             Se lanzará una excepción en caso que se presente un error
	 *             durante la validación.
	 */
	protected static void validateIncidentPoins(
			final GeometricDomain geometricDomain,
			List<GeometricDomainFault> faults) throws GeoInfoException {
		
		List<Point> points = geometricDomain.getPoints();
		List<LineSegment> lineSegments = geometricDomain.getLineSegments();
		if (points != null && !points.isEmpty() 
			&& lineSegments != null && !lineSegments.isEmpty())
		{		
			for (Point point: geometricDomain.getPoints())
			{
				for (LineSegment lineSegment: lineSegments)
				{
					if ( !lineSegment.getBeginNode().getCoordinate().equals(point.getCoordinate())
					     && !lineSegment.getEndNode().getCoordinate().equals(point.getCoordinate())
					     && lineSegment.intersects(point)
					     && lineSegment.intersection(point) != null
					     && lineSegment.intersection(point).equals(point)
					   )					     
					{
						LOGGER.info(lineSegment.intersection(point).getCoordinate());
						GeometricDomainFault geometricDomainFault = new GeometricDomainFault( point, EGeomDomainClosureConditionType.INCIDENT_POINT_ON_LINE_SEGMENT );
						faults.add( geometricDomainFault );
						LOGGER.info("FAULT: " + geometricDomainFault.getLocation().toString() + "-" + geometricDomainFault.getType().getDescription());
					}
				}
			}
		}
	}

	/**
	 * Valida que todo punto de intersección entre dos segmentos de línea que se
	 * intersecten, haga parte o sea miembro del conjunto de puntos P del
	 * dominio geométrico.
	 * 
	 * @param geometricDomain
	 * @param faults
	 *            Lista donde se agregarán los puntos donde se incumpla la
	 *            cuarta condición: "Si dos segmentos de línea en S se
	 *            intersectan en un punto, dicho punto debe ser miembro del
	 *            conjunto de puntos P del dominio geométrico".
	 * @throws GeoInfoException
	 *             Se lanzará una excepción en caso que se presente un error
	 *             durante la validación.
	 */
	protected static void validateLineSegmentIntersections(
			final GeometricDomain geometricDomain,
			List<GeometricDomainFault> faults) throws GeoInfoException {		
		
		List<LineSegment> lineSegments1 = geometricDomain.getLineSegments();
		List<LineSegment> lineSegments2 = geometricDomain.getLineSegments();
		List<Point> points = geometricDomain.getPoints();
		Set<GeometricDomainFault> fourthFaults = new HashSet<GeometricDomainFault>();
		if (lineSegments1 != null && !lineSegments1.isEmpty())
		{
			if (points == null)
			{
				points = new ArrayList<Point>();
			}
			for (LineSegment lineSegment : lineSegments1) {
				for (LineSegment lineSegment2 : lineSegments2)
				{
					if ( !lineSegment.equals(lineSegment2) )
					{
						Geometry intersection = GeometryFactory.createPoint((lineSegment.intersection(lineSegment2)).getCoordinates());
						if (intersection != null
							&& intersection.getCoordinates().length > 0
							&& intersection.toText().contains("POINT")
							&& !(intersection.getCoordinate().equals(lineSegment.getBeginNode().getCoordinate()))
							&& !(intersection.getCoordinate().equals(lineSegment.getEndNode().getCoordinate())))							
						{
							boolean exists = false;
							Point intPoint = GeometryFactory.createPoint((com.vividsolutions.jts.geom.Point)lineSegment.intersection(lineSegment2)) ;
							for (Point point: points)
							{							
								if ( point.getCoordinate().equals(intPoint.getCoordinate()) )
								{
									exists = true;
								}
							}
							if ( !exists )
							{
								GeometricDomainFault geometricDomainFault = new GeometricDomainFault( intPoint, EGeomDomainClosureConditionType.INT_POINT_LINE_SEGMENTS );
								if ( !fourthFaults.contains(geometricDomainFault) )
								{
									fourthFaults.add( geometricDomainFault );
									LOGGER.info("FAULT: " + geometricDomainFault.getLocation().toString() + "-" + geometricDomainFault.getType().getDescription());
								}
							}
						}						
					}
				}
			}
		}
		faults.addAll(fourthFaults);
	}
	
	/**
	 * Verifica si el punto recibido como parámetro hace parte de la retícula especificado.
	 * 
	 * @param point El punto a evaluar sobre la retícula.
	 * @param grid Una grilla con un origen, un número de elementos y un tamaño de intervalo.
	 * @return Verdadero si el punto hace parte de la retícula, falso en otro caso.
	 */
	public static boolean verifyPointOnGrid( Point point, Grid grid )
	{
		int originX= (int) grid.getOrigin().getX();
		int originY= (int) grid.getOrigin().getY();
		int intSize = grid.getIntSize();
		int minX =   originX - ((int) Math.floor(grid.getNumPointsX()/2)*intSize);
		int maxX =   originX + ((int) Math.ceil (grid.getNumPointsX()/2)*intSize);
		int minY =   originY - ((int) Math.floor(grid.getNumPointsY()/2)*intSize);
		int maxY =   originY + ((int) Math.ceil(grid.getNumPointsY()/2)*intSize);
		return ((int)point.getX() >= minX && (int)point.getX() <= maxX) //Rango X
		       && ((int)point.getY() >= minY && (int)point.getY() <= maxY)
		       && (point.getX() - originX ) % intSize == 0
		       && (point.getY() - originY ) % intSize == 0;
		
	}
	
	protected static boolean containsByGeometry(Point point, List<Point> points)
	{
		for (Point auxPoint : points)
		{
			if ( auxPoint.getCoordinate().equals(point.getCoordinate()))
			{
				return true;
			}
		}
		return false;
	}
}
